Struct 跟 Class 長得很像,連同使用方式都很像,這麼相似的兩個 Object Type,勢必會被拿來比較,但是這兩個到底什麼像,什麼不像,讓我們來看看。
先來看一下 Struct 的語法:
struct 結構名稱 {
...
}
Struct 是使用 struct
關鍵字,後面接著結構名稱,至於結構名稱的命名與類別命名相同,一樣都是使用 Upper Camel Case。
struct Dog {
...
}
Struct 跟類別的特性幾乎是一模一樣,一樣有屬性
、方法
以及初始化
。
struct Dog {
let id: Int
var name: String
var breed: String
func run() {
print("RUN!")
}
}
咦?初始化呢?
為什麼沒跳出沒有初始化的錯誤訊息?
如果沒有定義初始值,不是應該透過
init()
執行建構式初始化?
沒有錯,在 Class 確實要這麼做,但是在 Struct 就不必這麼麻煩,因為會自動生成 initializer,像這種 Implicit Initializer 的稱作為 Memberwise Initializer
。
有了 Memberwise Initializer
,他會自動幫我們把 Struct 中所有屬性都加進建構式中作為參數輸入,所以在實體化的時候,就可以直接使用這樣的建構式來建立實體:
let dog = Dog(id: 7533967, name: "皮皮", breed: "柯基")
但是你還是可以使用自訂的 initializer:
struct Dog {
let id: Int
var name: String
var breed: String
init(id: Int, name: String) {
self.id = id
self.name = name
self.breed = "柯基"
}
func run() {
print("RUN!")
}
}
let dog = Dog(id: 7533967, name: "皮皮")
但是,當我們建立一個自定義的 Initializer 時,Memberwise Initializer
就不會被產生,自然就不能使用 Dog(id:name:breed:)
來建立實體。
在談談 Struct 不能繼承這件事之前,首先,我們先來了解什麼是繼承?
繼承( Inheritance ),是物件導向程式設計中其中一個特性,假使有一個 類別 A
繼承了 類別 B
,這時候類別 A
具有本身的屬性、方法和初始值之外,也同時具有類別 B 的。
來舉一個簡單的例子,在《 Day 20 | Swift Class 與 Struct 快樂二選一:Class 篇 》中提到的 Car
類別,車子有很多種累,Super Car
也算是 Car
的一種,一樣都會有輪胎、車子烤漆及品牌;一樣可以發動引擎,在這種具有共通性的情況下,避免撰寫類似的程式碼,我們就可以透過繼承,來減少重複出現的程式碼。
class Car {
var color: String
var brand: String
var wheelSize: Int
init(color: String, brand: String, wheelSize: Int) {
self.color = color
self.brand = brand
self.wheelSize = wheelSize
}
func engineStart() {
print("拉風 引擎發動!")
}
}
class SuperCar: Car {
}
SuperCar
繼承 Car
,所以是 Car
的子類別,這時候就可以使用父類別的屬性、方法及建構式。
let superCar = SuperCar(color: "Verde Mantis", brand: "Lamborghini", wheelSize: 21)
print(superCar.color, superCar.brand, superCar.wheelSize)
superCar.engineStart()
// VerdeMantis Lamborghini 21
// 拉風 引擎發動!
但是 Super Car
也會有一些 Car
沒有的東西,還是可以在 Super Car 中,加入屬於自己類別的屬性或方法:
class SuperCar: Car {
var mode: String
init(color: String, brand: String, wheelSize: Int, mode: String) {
self.mode = mode
super.init(color: color, brand: brand, wheelSize: wheelSize)
}
func turnToMode() {
switch self.mode {
case let mode:
print("Mode: \(mode)")
}
}
}
let superCar = SuperCar(color: "Verde Mantis", brand: "Lamborghini", wheelSize: 21, mode: "極速模式")
superCar.turnToMode()
// Mode: 極速模式
或是如果想要更改父類別的屬性或是方法,可以透過 override
的前綴字來複寫:
override func engineStart() {
super.engineStart()
print("超跑引擎已發動")
}
透過 override
來複寫 Car
的 engineStart()
方法,所以在調用上就會是 SuperCar 的 engineStart()
:
superCar.engineStart()
// 拉風 引擎發動!
// 超跑引擎已發動
所以繼承能夠避免重複撰寫相似的程式碼,並且在同類型的類別中,又能在客製自己獨特的屬性、方法或是 Initializer。
扯這麼多,Struct 就是不能做到這件事情,但也不是完全不能繼承哪個有錢的老爸,還是有方法可以做到類似繼承,並且比繼承還好,先提示一下,那個大招叫做 Protocol
,更多細節請看接下來的幾個章節 ><
咦?怎麼突然提到這個?
Class 跟 Struct 最大的差別就在於:
那什麼是 Reference Type?Value Type 又是哪位?
直接來看範例最清楚:
struct People {
var name: String
}
var people1 = People(name: "張三")
var people2 = people1
print("1: \(people1.name), 2: \(people2.name)")
// 1: 張三, 2: 張三
people1.name = "李四"
print("1: \(people1.name), 2: \(people2.name)")
// 1: 李四, 2: 張三
建立了一個 People
的實體 people1
,再指派給 people2
,這時候兩個的 name
相同,但如果更改了 people1
的 name
,但是 people2
卻還是沒變,這就是 Value Type。
雖然 people2 是 people1 指派過去的,將內容複製一份過去,但兩者則是分別佔據不同的記憶體空間,所以不會互相影響。
withUnsafeMutablePointer(to: &people1) {
print($0) // 0x0000000109c92750
}
withUnsafeMutablePointer(to: &people2) {
print($0) // 0x0000000109c92760
}
反之 Reference Type 則會共享同一個記憶體位址:
class People {
var name = "張三"
}
let people1 = People()
let people2 = people1
print("1: \(people1.name), 2: \(people2.name)")
// 1: 張三, 2: 張三
people1.name = "李四"
print("1: \(people1.name), 2: \(people2.name)")
// 1: 李四, 2: 李四
print(Unmanaged.passUnretained(people1).toOpaque())
// 0x0000600003522a80
print(Unmanaged.passUnretained(people2).toOpaque())
// 0x0000600003522a80
也因為共享同一個記憶體位址,所以當一個物件屬性變動時,另一個也會跟著變動。
至於 Struct 與 Class 的抉擇,Apple 在官方文件上有提到了四點:
在以往 OOP 的觀念下,通常都會優先使用 Class 作為資料存取的優先考量,但是 Apple 持續地推廣 協定導向設計模式 (POP, Protocol Oriented Programming)
的觀念,有別繼承這個特性,有了新的觀點,也使得 Struct 有了更多合理的使用理由,至於 OOP 與 POP 的差異,會在接下來詳細討論。